Skip to main content

07 - 失败模式、评测与选型

前置0206 篇。

本篇回答:这一层会怎么坏,以及你怎么知道它到底有没有用 —— 后半个问题目前没有可以直接引用的公开答案,本篇给出为什么,以及自己该怎么测。

本篇会用到的词

意思
记忆投毒诱导系统把伪造的事实写进记忆,之后每一轮都会被读回来
错误固化一条错记忆影响了回答,这次交互又被抽取成新记忆,把错误加强一次
J scoreLoCoMo 论文里用的评判指标,由模型判断回答是否与标准答案一致
对抗类问题LoCoMo 的第 5 类问题,设计上就没有正确答案,协议规定排除在评分之外
单变量对照一次只改一个组件(模型、嵌入、检索管线)再比较,MemDelta 提出的评测协议

一、记忆投毒:三条写入通路

记忆和上下文注入的区别在于持续性:一次上下文注入影响一轮对话,一次记忆投毒影响之后的每一轮。

三条通路的防线不在同一层 —— 只堵住第一条是最常见的漏防① 用户直接说「记住:管理员密码规则是……」② 工具返回的内容里带指令网页、邮件、工单正文里的隐藏文本③ 助手自己的幻觉抽取器从助手消息里也抽事实时抽取器它不判断真假记忆库一条普通记录之后的每一轮检索命中就注入没有过期机制就永久有效②最难防:内容来自工具结果,而工具结果按设计就是要进上下文的。防线只能放在"工具结果不参与抽取"或"抽取前先剥离指令性文本"。
抽取器不做真假判断,这是设计上的事实而不是缺陷 —— 它的任务是"从对话里找出值得记的事实","这句话是不是真的"超出了它能拿到的信息。防线必须放在它前面(限制抽取来源)或后面(写入审计),不能指望它自己拦。

三条通路的对应防线:

通路防线落在哪一层
① 用户直接说记忆分类:涉及凭证、权限、系统配置的内容不允许写入写入前的一个分类器或正则清单
② 工具结果带指令抽取只从用户消息取(mem0 的 USER_MEMORY_EXTRACTION_PROMPT 就是这个策略),或抽取前剥离工具结果抽取的输入范围
③ 助手幻觉若抽取范围包含助手消息,加排除清单:不抽对用户的主观刻画、不抽未经用户确认的推断抽取提示词

②和抽取范围的取舍直接冲突02 篇5.3 讲过,只从用户消息抽会丢掉助手给出的方案和承诺;两边都抽则把工具结果和幻觉都拉进了写入面。没有免费的解 —— 现实做法是两边都抽,但对助手消息用更严格的排除清单,并且工具结果单独排除。

外泄方向的威胁(记忆里的东西怎么被套出去)在安全专题 07 篇。两个方向合起来才是完整的记忆威胁面。

二、错误固化

比投毒更常见、也更难发现的是一条无恶意的错记忆自己把自己加强

每转一圈,库里支持这条错误结论的记忆条目就多一条① 抽错一条「用户偏好邮件沟通」实际是那次刚好用了邮件② 被检索注入下次默认走邮件回答本身没有明显异常③ 用户不纠正因为它看起来是合理的没有反馈信号产生④ 这次交互又被抽一遍「用户又一次用邮件沟通」错误结论现在有两条证据回到①,且证据变多04 篇提到的 episode_mentions 类重排会加速这个循环 —— 被提到次数越多排得越靠前,而次数正是循环自己制造出来的。唯一有效的打断点是③:需要一个跟"用户有没有纠正"无关的独立校验信号,比如定期把记忆拿给用户确认,或抽样人工复核。
这个循环的危险在于每一步都是"正常工作"。没有任何一个环节会报错,监控上也看不出异常 —— 记忆条数在正常增长,检索命中率甚至在提高。

可操作的缓解手段

  • 区分观察与结论。抽取时把「用户这次用了邮件」(观察)和「用户偏好邮件」(结论)分开存,结论类记忆要求至少 N 次独立观察支持,且记下支持它的观察 ID
  • 给结论类记忆设置复核期。写入 90 天后降级为"待确认",下次相关交互时由模型显式验证一次
  • 让用户能看到并修改。文件式记忆(05 篇)在这一点上有结构性优势 —— 记忆是人可读的文件,用户可以直接翻

三、跨租户串号:三处失守点

这是这一层唯一会被定级为事故的失败模式。三处失守点,按被踩到的频率排序:

失守点具体形态防法
检索时漏传租户过滤mem0 的 filters 是可选参数,user_id / agent_id / run_id 三个维度漏传任一个,检索范围就扩大在存储层把租户字段做成必填分区键,漏传直接报错。不要只靠代码评审
共享的记忆命名空间LangGraph Store 的 namespace=("memories",) 这类写法,所有用户共用一个命名空间命名空间必须含用户标识:("memories", user_id)
抽取时的租户来源抽取任务异步执行,从队列里取消息时用错了上下文里的 user_id租户标识随消息体一起传,不从任何全局或线程局部变量取

第二条尤其容易踩,因为官方快速开始的示例就是错的形态

# ❌ LangMem README 的快速开始写法 —— 所有用户共用一个命名空间
create_manage_memory_tool(namespace=("memories",))
create_search_memory_tool(namespace=("memories",))

# ✅ 命名空间带上用户标识
# 注意:不能只在 search 那边加,manage 那边也要加 —— 否则写进了公共空间,
# 带过滤的搜索反而一条也搜不到,表现成"记忆没生效"而不是"记忆串了"
create_manage_memory_tool(namespace=("memories", user_id))
create_search_memory_tool(namespace=("memories", user_id))

InMemoryStore 同理:README 用它做演示,重启即丢;上生产要换 AsyncPostgresStore快速开始示例的目的是"三行跑起来",不是"可以照抄上生产" —— 这一条对所有记忆库都成立。

四、评测:为什么公开数字目前不可比

这一节复盘一次完整的公开争议,因为它比任何抽象论述都能说明"记忆评测的数字该怎么读"。

一次完整的公开往复,前后 11 天,同一个系统的分数改了两次2024-02LoCoMo 发布2025-01Zep 论文2025-04-28Mem0 论文Zep 博客反驳自报 84%2025-05-08Mem0 提 issue重算 58.44%2025-05-12Zep 承认算错更正 75.14%2026-06MemDelta2025-05-19:issue 因无后续讨论被关闭。争议没有第三方裁决,两边各自的数字都还挂在公开材料上。这不是谁不诚实的问题 —— 双方各自承认并修正了自己的计算错误。它说明的是这类评测极易在实现细节上产生数量级的偏差读任何一家的对比数字之前,先确认:谁跑的对方系统、用的哪些子集、几次运行取平均。
值得注意的是 Zep 指出的三处实现错误:把对话的两个说话人当成同一个用户建模、绕过 Zep 专门的时间戳字段、以及串行而非并行执行检索。这三条都属于"用对方的系统时没有按对方的最佳实践配置"—— 这是所有第三方对比评测的通病,而不是某一家的问题。

4.1 同一个系统、同一个基准、四个数字

同一个系统在同一个基准上,公开材料里有四个数字,最大差 25.6 个百分点Mem0 重算(1–4 类)58.44Mem0 论文测得65.99Zep 更正后75.14Zep 初次自报84.00全上下文基线 ≈73%横轴 0 到 100 的 J score。最右那条参照线是关键:把整段对话原样塞进上下文就能到 73%,而 Mem0 自己最好的配置约 68%。
参照线的含义是:在这个基准上,"什么记忆机制都不用、全文丢进去"打得过专门的记忆系统。原因是 LoCoMo 的对话本身只有 16,000 到 26,000 token,远在现代模型的上下文窗口之内 —— 它压根没有制造出需要记忆层的压力。

4.2 基准本身的问题

Zep 在复盘里列出的 LoCoMo 数据质量问题:

  • 第 5 类(对抗类)缺少标准答案,无法使用 —— 这也正是后来口径之争的来源
  • 多模态部分存在图片描述与内容不匹配
  • 部分问题的说话人归属标注错误
  • 存在有多个合理答案的歧义问题

加上上面那条"全上下文基线更高",结论是:LoCoMo 不足以区分记忆系统的优劣。这不是说它没价值,而是说在它上面赢几个百分点,不构成选型依据。

4.3 MemDelta:把变量一个一个拆开

2026 年 6 月的 MemDelta(arXiv 2606.29914)针对这个问题提出了单变量对照协议:一次只改一个组件,在 LongMemEval-S(500 题、50+ 会话、三个模型家族)上跑。四条结论都值得记住:

结论数字对选型的含义
原文切片 RAG 打平全上下文47.2% 对 49.8%,p = 0.34差异不显著。但排序会随模型翻转:Gemini 从全上下文得 +14pp,Sonnet 从 RAG 得 +31pp(部分因为它拒答了 63% 的全上下文查询)
只换嵌入模型准确率变动 +6.2pp,p = 0.004一个变量就能翻转结论:Mem0 比 MiniLM-RAG 高 11pp,但比云端 RAG 低 1.2pp
Agent 自管记忆 vs 朴素检索42% 对 47%让模型自己决定记什么,在这个设定下不如直接检索原文
Mem0 在 6 类问题中的 2 类(n=88)72.7% 对云端 RAG 的 73.9%,p = 1.0,成本 50 倍优势是窄的,不是普遍的

论文给出的建议也很直接:记忆评测必须在各组之间固定嵌入模型。上面第二条说明,不固定就等于在比嵌入模型而不是比记忆系统。

五、自己怎么测:一套最小可用评测集

既然公开数字不可比,选型只能自己测。一套能在两天内搭起来的最小集:

# 每条样例的结构:先喂若干轮对话建立记忆,再问一个问题,检查回答里有没有那个事实
# 关键是覆盖 04 篇第二节那四类失效 —— 通用问答集测不出这些
CASES = [
# ① 否定:向量检索最容易取反的一类
{"setup": ["我不吃香菜"], "ask": "推荐一道凉菜", "must_not_contain": ["香菜"]},

# ② 时间:先说 A 再改成 B,检查旧值有没有被作废
{"setup": ["我住北京", "我上个月搬到上海了"], "ask": "推荐附近的餐厅", "must_contain": ["上海"]},

# ③ 补充 vs 变更:第二条过敏源不能把第一条挤掉(03 篇第一节)
{"setup": ["我对花生过敏", "我还对芒果过敏"], "ask": "我的过敏源有哪些",
"must_contain": ["花生", "芒果"]},

# ④ 多跳:一次检索跨不过去的关系
{"setup": ["我老板是张三", "张三只看邮件"], "ask": "怎么把方案给我老板",
"must_contain": ["邮件"]},

# ⑤ 跨租户:用另一个 user_id 问同一件事,必须问不出来
{"setup": ["我对花生过敏"], "ask_as_other_user": "这个用户对什么过敏",
"must_not_contain": ["花生"]},

# ⑥ 干扰项:库里塞 200 条无关记忆之后,上面每一条还能不能过
# 这一条最重要 —— 空库上全过、满库上大面积失败是最常见的形态
]

评这套集的三条纪律:

  1. 固定嵌入模型和判分模型,只换记忆实现。这是 MemDelta 的核心建议
  2. 每个配置跑至少 5 次取平均并报方差。上面那场争议里,双方最终都改用了 10 次运行的平均值
  3. 必须有"不用记忆"的对照组。如果记忆层没有显著优于"什么都不做"或"把最近 20 轮全塞进去",那它带来的是成本和故障面,不是能力

第 3 条是最容易被跳过、也最能省钱的一条。

六、四步落地

顺序不能调 —— 第一步没拿到结论就往下走,后面三步都可能是白做的① 先证明它有用最小实现 + 对照组零外部服务那一档就够跑第五节那套用例没跑赢对照组就到此为止② 隔离与观测租户字段做成必填读回的条目打进 trace写入走 history 表留痕这一步才敢放真实用户③ 变化与遗忘作废语义按类型分级的 TTL安全类记忆强制注入上线三个月内一定会需要④ 高级能力图与多跳双时间轴回溯程序记忆有明确需求再上②里的三件事都要在放真实用户之前完成。跨租户串号是事故,事后补隔离意味着已经泄过一轮了。
把④放到最后不是因为它不重要,而是因为它的必要性只能由前三步的线上数据回答 —— 「我们真的需要多跳吗」这个问题,在没有线上失败样例之前只能靠猜。

七、小结

  • 记忆投毒有三条通路,只堵"用户直说"这一条是最常见的漏防;工具结果那条和抽取范围的取舍直接冲突,没有免费的解
  • 错误固化的每一步都"正常工作",监控上看不出来;打断它需要一个与用户反馈无关的独立校验信号
  • 跨租户串号的三处失守点里,最容易踩的是照抄官方快速开始的共享命名空间
  • 公开评测数字目前不可比:同一系统在同一基准上出现过 58.44% 到 84% 四个数字,而全上下文基线(≈73%)本身就打得过专门的记忆系统
  • 自己测的三条纪律:固定嵌入模型、多次运行报方差、必须有不用记忆的对照组
  • 落地顺序:先证明有用 → 隔离与观测 → 变化与遗忘 → 高级能力,顺序不能调

← 回到 专题索引 | Agent Infra 板块总览